﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Transactions;

namespace WorkNodeFramework
{
    public class WorkNodeService
    {
        public const int WorkNodeMaxRow = 10000;
        public const int MinMaxRow = 1000;
        public static readonly DateTime MinDate = new DateTime(1753, 1, 1, 0, 0, 0);
        public static readonly DateTime MaxDate = new DateTime(9999, 12, 31, 23, 59, 59);
        static WorkNodeService _singleton;

        public WorkNodeService(DataProvider provider) {
            this.provider = provider;
        }

        public WorkNodeService() {
        }

        public static WorkNodeService Default {
            get {
                if (_singleton == null)
                    _singleton = new WorkNodeService();
                return _singleton;
            }
        }

        DataProvider provider;
        public DataProvider Provider {
            get {
                if (provider == null)
                    return DataService.Provider;
                return provider;
            }
        }

        public WorkNodeTable WorkNodeTable(string baseName) {
            if (!Provider.TableProvider.Exist(baseName))
                Provider.TableProvider.Create(new WorkNodeTable(Provider, baseName));
            return Provider.GetTable<WorkNodeTable>(baseName);
        }

        public WorkNodePropertiesTable WorkNodePropertiesTable(string baseName) {
            if (!Provider.TableProvider.Exist(baseName))
                Provider.TableProvider.Create(new WorkNodePropertiesTable(Provider, baseName));
            return Provider.GetTable<WorkNodePropertiesTable>(baseName);
        }

        public WorkNodeLangPropertiesTable WorkNodeLangPropertiesTable(string baseName) {
            if (!Provider.TableProvider.Exist(baseName))
                Provider.TableProvider.Create(new WorkNodeLangPropertiesTable(Provider, baseName));
            return Provider.GetTable<WorkNodeLangPropertiesTable>(baseName);
        }

        public T GetNode<T>(string path, string name)
            where T : WorkNode
        {
            return GetNode<T>(path, name, null);
        }

        public T GetNode<T>(string path,string name, DateSetting dateSetting) 
            where T:WorkNode
        {
            path = WorkNodeUtility.TrimsPath(path);
            WorkPart wp = GetMainPart(path);
            if(wp == null)
                return null;
            //=========================
            string wnPath = path.Substring(wp.Path.Length);
            string wnTypeName = WorkNodeUtility.GetWorkNodeType<T>().Name;
            WorkNodeTable table = Provider.GetTable<WorkNodeTable>(wp.WorkNode);
            //=========================
            Select select = new Select(Provider)
                .From(table)
                .Where
                (
                    WHERE.DO(table.NameColumn).IsEqualsTo(name),
                    WHERE.DO(table.TypeColumn).IsEqualsTo(wnTypeName),
                    WHERE.DO(table.PathColumn).IsEqualsTo(wnPath)
                );
            
            if (dateSetting != null) {
                Column dateCol =dateSetting.DateType == DateType.OpenDate?table.OpenDateColumn:table.LastDateColumn;
                dateSetting.UpdateSelectQuery(select, dateCol);
            }

            T node = select.ExecuteEntity<T>();
            if (node != null)
            {
                node.FullPath = path;
                node.WorkPart = wp;
                return node;
            }

            foreach (WorkPart p in GetParts(path,MinDate,MaxDate)) {
                if(p.IsMain)
                    continue;
                table = Provider.GetTable<WorkNodeTable>(p.WorkNode);
                //=====================
                select = new Select(Provider)
                .From(table)
                .Where
                (
                    WHERE.DO(table.NameColumn).IsEqualsTo(name),
                    WHERE.DO(table.TypeColumn).IsEqualsTo(wnTypeName),
                    WHERE.DO(table.PathColumn).IsEqualsTo(wnPath)
                );

                if (dateSetting != null)
                {
                    Column dateCol = dateSetting.DateType == DateType.OpenDate ? table.OpenDateColumn : table.LastDateColumn;
                    dateSetting.UpdateSelectQuery(select, dateCol);
                }
                //=====================
                node = select.ExecuteEntity<T>();
                if (node != null) {
                    node.FullPath = path;
                    node.WorkPart = p;
                    return node;
                }
            }

            return null;
        }

        public WorkNodeReader<T> GetNodes<T>(string path, params Predicate<T>[] predicates)
           where T : WorkNode
        {
            return GetNodes<T>(path, null, predicates);
        }

        public WorkNodeReader<T> GetNodes<T>(string path, DateSetting dateSetting, params Predicate<T>[] predicates)
           where T : WorkNode
        {
            int top = int.MaxValue;
            return GetNodes<T>(path, top, dateSetting, predicates);
        }

        public WorkNodeReader<T> GetNodes<T>(string path, int top, params Predicate<T>[] predicates)
           where T : WorkNode
        {
            return GetNodes<T>(path, top, null, predicates);
        }

        public WorkNodeReader<T> GetNodes<T>(string path, int top, DateSetting dateSetting, params Predicate<T>[] predicates)
           where T : WorkNode
        {
            return GetNodes<T>(path, 0, top, dateSetting,predicates);
        }

        public WorkNodeReader<T> GetNodes<T>(string path, int pageIndex, int pageCount, params Predicate<T>[] predicates)
            where T : WorkNode
        {
            return GetNodes<T>(path, pageIndex, pageCount, null, predicates);
        }

        public WorkNodeReader<T> GetNodes<T>(string path, int pageIndex, int pageCount, DateSetting dateSetting, params Predicate<T>[] predicates)
            where T:WorkNode
        {
            if (dateSetting != null && dateSetting.DateType == DateType.LastDate)
                throw new NotSupportedException("Not support for DateType is LastDate");
            //=========================
            path = WorkNodeUtility.TrimsPath(path);
            //=========================
            WorkPart _wp = GetMainPart(path);
            if (_wp == null)
                return new WorkNodeReader<T>(new NodeEnumerator<T>[0]);
            DateTime from = MinDate;
            DateTime to = MaxDate;

            if(dateSetting!=null)
            {
                if(dateSetting.From!=null)
                    from = dateSetting.From.Value;
                if(dateSetting.To!=null)
                    to = dateSetting.To.Value;
            }

            string wnPath = path.Substring(_wp.Path.Length);
            WorkPart[] wpList = GetParts(_wp.Path, from, to);
            List<NodeEnumerator<T>> nodeEnumerators = new List<NodeEnumerator<T>>();
            foreach (WorkPart wp in wpList)
            {
                WorkNodeTable wnTable = Provider.GetTable<WorkNodeTable>(wp.WorkNode);
                Provider.GetTable<WorkNodePropertiesTable>(wp.Properties);
                Provider.GetTable<WorkNodeLangPropertiesTable>(wp.LangProperties);
                //=====================
                WorkNodeTypeAttribute wnType = WorkNodeUtility.GetWorkNodeType<T>();
                //=========================
                Select select = GetSelect(wnTable, wnPath, wnType.Name);
                if (dateSetting != null)
                {
                    Column dateCol = dateSetting.DateType == DateType.OpenDate ? wnTable.OpenDateColumn : wnTable.LastDateColumn;
                    dateSetting.UpdateSelectQuery(select, dateCol);
                }
                nodeEnumerators.Add(new NodeEnumerator<T>(select, select.Provider,wp));
            }
            return new WorkNodeReader<T>(nodeEnumerators.ToArray(), predicates, pageIndex, pageCount);
        }

        Select GetSelect(WorkNodeTable wnTable, string wnPath, string wnName)
        {
            Select select = new Select(wnTable.Provider)
                .From(wnTable)
                .Where
                (
                    WHERE.DO(wnTable.PathColumn).IsEqualsTo(wnPath),
                    WHERE.DO(wnTable.TypeColumn).IsEqualsTo(wnName)
                );
                
            return select;
        }

        IDataReader GetDataReader(WorkNodeTable wnTable,string wnPath,string wnName)
        {
            IDataReader reader = new Select(wnTable.Provider)
                .From(wnTable)
                .Where
                (
                    WHERE.DO(wnTable.PathColumn).IsEqualsTo(wnPath),
                    WHERE.DO(wnTable.TypeColumn).IsEqualsTo(wnName)
                )
                .ExecuteReader();
            return reader;
        }

        public void DeleteNodes<T>(string path, params Predicate<T>[] predicates)
            where T:WorkNode
        {
            DeleteNodes<T>(path, null, predicates);
        }

        public void DeleteNodes<T>(string path,DateSetting dateSetting,params Predicate<T>[] predicates)
            where T:WorkNode
        {
            List<T> nodes = GetNodes<T>(path, dateSetting,predicates).ToList();
            DeleteNodes<T>(nodes.ToArray());
        }

        public void DeleteNodes<T>(params T[] nodes)
            where T : WorkNode
        {
            if (nodes.Length == 0)
                return;
            List<ExecuteCommandManager> cmList = new List<ExecuteCommandManager>();
            foreach (T node in nodes)
                cmList.Add(node.WorkPart.GetDeleteCommandManager(node));
            Provider.ExecuteTransaction(cmList.ToArray());
        }

        public void UpdateNodes<T>(Setter<T> setter, string path, params Predicate<T>[] predicates)
            where T:WorkNode
        {
            UpdateNodes<T>(setter, path, null, predicates);
        }

        public void UpdateNodes<T>(Setter<T> setter, string path, DateSetting dateSetting, params Predicate<T>[] predicates)
            where T:WorkNode
        {
            List<T> nodes = GetNodes<T>(path, dateSetting, predicates).ToList();
            foreach (T node in nodes)
                setter(node);
            UpdateNodes<T>(nodes.ToArray());
        }

        public void UpdateNodes<T>(params T[] nodes)
            where T:WorkNode
        {
            if (nodes.Length == 0)
                return;
            List<ExecuteCommandManager> cmList = new List<ExecuteCommandManager>();
            foreach (T node in nodes)
                cmList.Add(node.WorkPart.GetUpdateCommandManager(node));
            Provider.ExecuteTransaction(cmList.ToArray());
        }

        public void InsertNodes<T>(string path, params T[] nodes)
            where T:WorkNode
        {
            if (nodes.Length == 0)
                return;
            path = WorkNodeUtility.TrimsPath(path);
            WorkPart wp = GetMainPart(path);
            if (wp == null) {
                WorkNodeTypeAttribute wnType = WorkNodeUtility.GetWorkNodeType<T>();
                wp = CreatePart(path, wnType.Name, wnType.Properties, wnType.LangProperties, DateTime.Now, DateTime.MaxValue, WorkNodeMaxRow, true);
            }
            //=========================
            WorkNodeTable(wp.WorkNode);
            WorkNodePropertiesTable(wp.Properties);
            WorkNodeLangPropertiesTable(wp.LangProperties);
            //=========================
            string wnPath = path.Substring(wp.Path.Length);
            List<ExecuteCommandManager> cmList = new List<ExecuteCommandManager>();
            foreach (T node in nodes)
                cmList.Add(wp.GetInsertCommandManager(wnPath, node));
            Provider.ExecuteTransaction(cmList.ToArray());
        }

        public void Archives(string path)
        {
            bool success = false;
            bool needArchive = false;
            path = WorkNodeUtility.TrimsPath(path);
            AutomaticConnectionScope cs = new AutomaticConnectionScope(Provider);
            if (cs.Connection.State == ConnectionState.Closed)
                cs.Connection.Open();
            DbTransaction trans = cs.Connection.BeginTransaction(System.Data.IsolationLevel.ReadUncommitted);
            try
            {
                IExecuteCommand[] commands = DoArchives(path, cs.Connection, trans);
                needArchive = commands.Length > 0;
                Provider.ExecuteTransaction(cs.Connection, trans, commands);
                trans.Commit();
                success = true;
            }
            catch (SqlException ex)
            {
                trans.Rollback();
                throw ex;
            }
            finally
            {
                cs.Dispose();
            }

            if (success && needArchive)
            {
                ITable archiveTable = Provider.GetTable("Archives");
                WorkPart[] parts = GetParts(path, MinDate, MaxDate);
                Dictionary<string, ITable> tables = new Dictionary<string, ITable>();
                foreach (WorkPart wp in parts)
                    tables[wp.WorkNode] = Provider.GetTable<WorkNodeTable>(wp.WorkNode);
                List<ExecuteCommand> wpCommands = new List<ExecuteCommand>();
                foreach (WorkPart wp in parts)
                {
                    WorkNodeTable wnTable = (WorkNodeTable)tables[wp.WorkNode];
                    DateTime fromDate = (DateTime)new Select(Provider, wnTable.OpenDateColumn)
                        .Top(1)
                        .From(wnTable)
                        .Order(wnTable.OpenDateColumn, OrderByEnum.ASC)
                        .ExecuteScalar();
                   
                    DateTime toDate = (DateTime)new Select(Provider, wnTable.OpenDateColumn)
                        .Top(1)
                        .From(wnTable)
                        .Order(wnTable.OpenDateColumn, OrderByEnum.DESC)
                        .ExecuteScalar();
                    wp.FromDate = fromDate;
                    wp.ToDate = toDate;
                    wp.Save(archiveTable);
                }
            }
        }

        IExecuteCommand[] DoArchives(string path,DbConnection con, DbTransaction trans)
        {
            path = WorkNodeUtility.TrimsPath(path);
            WorkPart[] parts = GetParts(path, MinDate, MaxDate);
            if (parts.Length == 0)
                return new IExecuteCommand[0];
            int len = parts.Length - 1;
            Dictionary<string,ITable> tables = new Dictionary<string,ITable>();
            foreach (WorkPart wp in parts) {
                tables[wp.WorkNode] = Provider.GetTable<WorkNodeTable>(wp.WorkNode);
                tables[wp.Properties] = Provider.GetTable<WorkNodePropertiesTable>(wp.Properties);
                tables[wp.LangProperties] = Provider.GetTable<WorkNodeLangPropertiesTable>(wp.LangProperties);
            }

            List<WorkPart> wpList = new List<WorkPart>();
            List<int> wpCountList = new List<int>();

            for (int i = len; i >= 0; i--)
            {
                wpList.Add(parts[i]);
                wpCountList.Add(-1);
            }

            int index = 0;
            List<IExecuteCommand> commands = new List<IExecuteCommand>();
        REPEAT:
            for (int i = index; i < len; i++)
            {
                WorkPart curWP = wpList[i];
                //=====================
                ITable curWNT = tables[curWP.WorkNode];
                ITable curPT = tables[curWP.Properties];
                ITable curLPT = tables[curWP.LangProperties];
                //=====================
                int maxRow = curWP.MaxRow;
                if (maxRow == -1)
                    maxRow = WorkNodeMaxRow;
                else if (maxRow < MinMaxRow)
                    maxRow = MinMaxRow;
                int curCount = wpCountList[i];
                if (curCount == -1)
                {
                    curCount = new Select(Provider)
                        .From(curWNT)
                        .Count();
                    wpCountList[i] = curCount;
                }

                if (curCount >= maxRow)
                    continue;

                for (int j = i + 1; j <= len; j++)
                {
                    WorkPart wp = wpList[j];
                    //=================
                    WorkNodeTable wnt = (WorkNodeTable)tables[wp.WorkNode];
                    WorkNodePropertiesTable pt = (WorkNodePropertiesTable)tables[wp.Properties];
                    WorkNodeLangPropertiesTable lpt = (WorkNodeLangPropertiesTable)tables[wp.LangProperties];
                    //=================
                    int count = wpCountList[j];
                    if (count == -1)
                    {
                        count = new Select(Provider)
                            .From(wnt)
                            .Count();
                        wpCountList[j] = count;
                    }
                    if (count == 0)
                        continue;
                    int needInserts = maxRow - curCount;
                    int top = count < needInserts ? count : needInserts;
                    //=================
                    SelectQuery select = new SelectQuery()
                            .From(wnt)
                            .Top(top)
                            .Order(wnt.OpenDateColumn, OrderByEnum.ASC);
                    InsertInto ii = new InsertInto(curWNT);
                    ii.Query(select);
                    commands.Add(ii);

                    Select inSelect = new Select(Provider, wnt.IDColumn)
                                .Top(top)
                                .From(wnt)
                                .Order(wnt.OpenDateColumn, OrderByEnum.ASC);
                    select = new SelectQuery(pt.All)
                        .From(pt)
                        .Where(WHERE.DO(pt.WorkNodeIDColumn).In(inSelect));
                    ii = new InsertInto(curPT);
                    ii.Query(select);
                    commands.Add(ii);

                    select = new SelectQuery(lpt.All)
                        .From(lpt)
                        .Where(WHERE.DO(lpt.WorkNodeIDColumn).In(inSelect));
                    ii = new InsertInto(curLPT);
                    ii.Query(select);
                    commands.Add(ii);

                    Delete delete = new Delete(Provider)
                        .From(pt)
                        .Where(WHERE.DO(pt.WorkNodeIDColumn).In(inSelect));
                    commands.Add(delete);

                    delete = new Delete(Provider)
                        .From(lpt)
                        .Where(WHERE.DO(lpt.WorkNodeIDColumn).In(inSelect));
                    commands.Add(delete);

                    delete = new Delete(Provider)
                        .From(wnt)
                        .Where(WHERE.DO(wnt.IDColumn).In(inSelect));
                    commands.Add(delete);
                    //=================
                    count = count - top;
                    curCount = curCount + top;
                    wpCountList[i] = curCount;
                    wpCountList[j] = count;
                    if (curCount == maxRow)
                        continue;
                }
            }

            WorkPart mainPart = wpList[wpList.Count - 1];
            int mainCount = wpCountList[wpCountList.Count - 1];
            if (mainCount == -1)
            {
                mainCount = new Select(Provider)
                    .From(tables[mainPart.WorkNode])
                    .Count();
                wpCountList[wpCountList.Count - 1] = mainCount;
            }
            int mainMaxRow = mainPart.MaxRow;
            if (mainMaxRow == -1)
                mainMaxRow = WorkNodeMaxRow;
            else if (mainMaxRow < MinMaxRow)
                mainMaxRow = MinMaxRow;
            if (mainCount > mainMaxRow)
            {
                int newTableIndex = wpList.Count;
                while (Provider.TableProvider.Exist(mainPart.WorkNode + newTableIndex.ToString()))
                {
                    newTableIndex++;
                }
                string sufix = newTableIndex.ToString();
                string wnName = mainPart.WorkNode + sufix;

                WorkPart newWorkPart = new WorkPart();
                newWorkPart.Path = mainPart.Path;
                newWorkPart.MaxRow = mainMaxRow;
                newWorkPart.WorkNode = wnName;
                newWorkPart.Properties = newWorkPart.Properties;
                newWorkPart.LangProperties = newWorkPart.LangProperties;
                //=====================
                TableProvider tp = Provider.TableProvider;
                WorkNodeTable newWNT = new WorkNodeTable(Provider, newWorkPart.WorkNode);
                WorkNodePropertiesTable newPT = new WorkNodePropertiesTable(Provider, newWorkPart.Properties);
                WorkNodeLangPropertiesTable newLPT = new WorkNodeLangPropertiesTable(Provider, newWorkPart.LangProperties);
                Provider.ExecuteTransaction(con, trans,
                    tp.GetCreateCommand(newWNT),
                    tp.GetCreateCommand(newPT),
                    tp.GetCreateCommand(newLPT));
                tables[newWNT.Name] = newWNT;
                tables[newPT.Name] = newPT;
                tables[newLPT.Name] = newLPT;
                //=====================
                wpList.Insert(wpList.Count - 1, newWorkPart);
                wpCountList.Insert(wpCountList.Count - 1, 0);
                index = wpList.IndexOf(newWorkPart);
                len = wpList.Count - 1;
                goto REPEAT;
            }

            if (commands.Count > 0)
            {
                ITable archiveTable = Provider.GetTable("Archives");
                foreach (WorkPart wp in wpList)
                    if (((IFieldSet)wp).DefaultStatus == FieldSetStatus.New)
                    {
                        wp.FromDate = MinDate;
                        wp.ToDate = MaxDate;
                        IExecuteCommand cmd = wp.GetSaveCommand(archiveTable);
                        if (cmd != null)
                            commands.Add(cmd);
                    }
            }
            return commands.ToArray();
        }

        internal WorkPart CreatePart(string path, string workNode, string properties, string langProperties, DateTime from, DateTime to, int maxRow, bool isMain)
        { 
            path = WorkNodeUtility.TrimsPath(path);
            ITable table = Provider.GetTable(workNode);
            if (table != null) {
                return new Select(Provider)
                    .From(table)
                    .Where
                    (
                        WHERE.DO(table["Path"]).IsEqualsTo(path),
                        WHERE.DO(table["WorkNode"]).IsEqualsTo(properties)
                    )
                    .ExecuteEntity<WorkPart>();
            }
            //=========================
            WorkPart wp = new WorkPart();
            wp.Path = path;
            wp.WorkNode = workNode;
            if (!string.IsNullOrEmpty(properties))
                wp.Properties = properties;
            if (!string.IsNullOrEmpty(langProperties))
                wp.LangProperties = langProperties;
            wp.MaxRow = maxRow;
            wp.FromDate = from;
            wp.ToDate = to;
            wp.IsMain = isMain;
            wp.Provider = Provider;
            //=========================
            WorkNodeTable(wp.WorkNode);
            WorkNodePropertiesTable(wp.Properties);
            WorkNodeLangPropertiesTable(wp.LangProperties);
            //=========================
            wp.Save(table);
            return wp;
        }

        public WorkPart GetMainPart(string path)
        {
            path = WorkNodeUtility.TrimsPath(path);
            ITable table = Provider.GetTable("Archives");
            if(table==null)
                return null;
            Select sel = new Select(Provider)
                .From(table)
                .Where
                (
                    WHERE.DO(table["Path"]).IsEqualsTo(path),
                    WHERE.DO(table["IsMain"]).IsEqualsTo(true)
                );
            WorkPart wp = sel.ExecuteEntity<WorkPart>();
            if (wp != null)
            {
                wp.Provider = Provider;
                return wp;
            }

            int idx = path.LastIndexOf('/');
            if (idx == -1)
                return null;
            path = path.Substring(0, idx);
            return GetMainPart(path);
        }

        public WorkPart[] GetParts(string path, DateTime from, DateTime to)
        {
            WorkPart mainPart = GetMainPart(path);
            if (mainPart == null)
                return new WorkPart[0];

            WorkPart[] parts = GetParts(mainPart.Path, from, to, false);
            if (mainPart.FromDate < from || mainPart.ToDate > to)
                return parts;
            WorkPart[] result = new WorkPart[parts.Length + 1];
            for (int i = 0; i < parts.Length; i++)
                result[i+1] = parts[i];
            result[0] = mainPart;
            return result;
        }

        WorkPart[] GetParts(string path, DateTime from, DateTime to, bool includeMainPart)
        {
            if (from > to)
                throw new WorkNodeException("The from date must be equals or greater than the to date");
            ITable table = Provider.GetTable("Archives");
            Select select = new Select(Provider)
                .From(table)
                .Where
                (
                    WHERE.DO(table["Path"]).IsEqualsTo(path),
                    WHERE.NOT(WHERE.OR(
                        WHERE.DO(table["FromDate"]).IsGreaterThan(to),
                        WHERE.DO(table["ToDate"]).IsLeasterThan(from)
                    ))
                )
                .Order(table["ToDate"],OrderByEnum.DESC);
            if (!includeMainPart)
                select.Where(WHERE.DO(table["IsMain"]).IsEqualsTo(false));
            List<WorkPart> parts = select.ExecuteTypedList<WorkPart>();
            foreach (WorkPart part in parts)
                part.Provider = Provider;
            return parts.ToArray();
        }
    }
}
